package org.easiermock;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.aopalliance.intercept.MethodInvocation;

/**
 * The heart of the EasierMock behaviour. Implements the standard Java 
 * <code>InvocationHandler</code> interface for compatibility with Java 
 * dynamic proxies (but can be adapted for use with other frameworks). 
 * 
 * <p>The class wraps a delegate object and a MockContext. Any methods
 * called on the Proxy (or similar) class will be passed through to the
 * delegate <em>if</em> the delegate has a method with a compatible 
 * signature AND if that method (or its declaring class) is annotated with 
 * the <code>Mock</code> annotation.  When a suitable method is found, 
 * prior to the call on the delegate the <code>MethodListener</code>s 
 * in the <code>MockContext</code> are invoked. If there is no suitable 
 * method an <code>UnsupportedOperationException</code> is thrown.</p> 
 *  
 * 
 * @author Neil Fuller
 */
class DelegatingInvocationHandler implements InvocationHandler, DelegateHolder {
	private MockContext mockContext;
	private Object delegate;
	private Map<Method, Method> methodLookup;

	/**
	 * Creates a DelegatingInvocationHandler for the delegate in  
	 * the supplied context
	 * 
	 * @param delegate the delegate
	 * @param mockContext the context
	 */
	public DelegatingInvocationHandler(Object delegate, MockContext mockContext) {
		this.delegate = delegate;
		this.methodLookup = Collections.synchronizedMap(new HashMap<Method,Method>());
		this.mockContext = mockContext;
	}
	
	/**
	 * See class documentation for details.
	 */
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		
		Method mockMethod;
		if (methodLookup.containsKey(method)) {
			mockMethod = methodLookup.get(method);
		} else {
			mockMethod = findMatchingMockMethod(delegate.getClass(), method);
			methodLookup.put(method, mockMethod);
		}
		
		if (mockMethod == null) {
			throw new UnsupportedOperationException("Unsupported mock method=[" + method + "]");
		}
		
		MethodListener[] methodListeners = mockContext.getMethodListeners();
		if (methodListeners != null && methodListeners.length > 0) {
			MethodInvocation methodInvocation = AopUtil.createMethodInvocation(delegate, mockMethod, args);
			for (MethodListener methodListener : methodListeners) {
				methodListener.beforeInvocation(methodInvocation);
			}
		}
		return mockMethod.invoke(delegate, args);
	}

	/**
	 * Finds the method on the <code>delegateClass</code> that matches the 
	 * specified <code>method</code>'s signature. In order to match, the method
	 * must be annotated with the <code>Mock</code> annotation.
	 * 
	 * @param delegateClass the delegate class to search
	 * @param method the method to look for
	 * @return the matching Method or null if none match
	 */
	private Method findMatchingMockMethod(Class<?> delegateClass, Method method) {
		Method matchingMethod;

		try {
			matchingMethod = delegateClass.getMethod(method.getName(), method.getParameterTypes());
			if (matchingMethod.getReturnType() != method.getReturnType()) {
				throw new NoSuchMethodException("Return types not the same");
			}
			
			Mock mockAnnotation = AnnotationUtil.getMockMethodAnnotation(matchingMethod);
			if (mockAnnotation == null) {
				// There is a matching method but it's not on 
				// a Mock annotated class or method
				matchingMethod = null;
			}
		} catch (NoSuchMethodException e) {
			// No matching method
			matchingMethod = null;
		}
		return matchingMethod;
	}

	public Object getDelegate() {
		return delegate;
	}
}
